package org.gpp.proj1.bone;

import java.io.FileNotFoundException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import com.jme.math.Quaternion;
import com.jme.math.Vector3f;
import com.jme.scene.Node;

/**
 * 
 * @author Paolo Victor, paolovictor@gmail.com
 */
public class Bone extends Node {
	
	private static final long serialVersionUID = 6305654273817474690L;

	/**
	 * The bone's child bones
	 */
	private List<Bone>  boneChildren;
	
	/**
	 * The keyframe quaternions
	 */
	protected List<Quaternion> keyframeAngles;
	
	/**
	 * The keyframe length, in update ticks
	 */
	protected List<Float> keyframeLengths;
	
	/**
	 * The keyframe bone widths, in update ticks
	 */
	protected List<Float> keyframeBoneWidths;
	
	/**
	 * The animation map
	 */
	protected Map<String, Animation> animations;
	
	/**
	 * The current animation 
	 */
	protected Animation actualAnimation;
	
	/**
	 * The previous animation. Used to go back to last
	 * animation after a non looping animation 
	 */
	protected Animation lastAnimation;
	
	/**
	 * The current keyframe number
	 */
	protected int actualKeyframeNumber;
	
	/**
	 * The current keyframe length, in ticks
	 */
	protected float actualKeyframeLength;
	
	/**
	 * The ticks passed for the actual keyframe
	 */
	protected float elapsedTicks;

	/**
	 * The actual keyframe's quaternion
	 */
	protected Quaternion actualKeyframeQuaternion;
	
	/**
	 * The actual keyframe's bone width
	 */
	protected float actualKeyframeWidth;

	/**
	 * The next keyframe's quaternion
	 */
	protected Quaternion nextKeyframeQuaternion;
	
	/**
	 * The next keyframe's bone width
	 */
	protected float nextKeyframeWidth;
	
	/**
	 * The bone's end joint
	 */
	protected Node endJoint;
		
	/**
	 * Default constructor
	 * @param name The node's name
	 */
	public Bone( String name ) {
		
		super( name );
		
		assert name != null : "Cannot build a bone with a null name";
		
		this.setLocalRotation( new Quaternion() );
		this.boneChildren = new LinkedList<Bone>();
		
		this.keyframeAngles = new LinkedList<Quaternion>();
		this.keyframeLengths = new LinkedList<Float>();
		this.keyframeBoneWidths = new LinkedList<Float>(); 
		
		this.animations = new HashMap<String, Animation>();
		
		this.actualAnimation = null;
		
		this.endJoint = new Node( name + "endJoint" );
		
		super.attachChild( endJoint );
	}
	
	/**
	 * Constructor that reads the keyframes from a file
	 * @param name
	 * @param keyframeFile
	 * 
	 * @throws FileNotFoundException If the bone keyframe file isn't found 
	 * @throws InvalidKeyframeFileException If the keyframe file isn't valid
	 */
	public Bone( String name, String keyframeFile ) throws FileNotFoundException, InvalidKeyframeFileException {

		this( name );
		
		assert keyframeFile != null : "Cannot pass a null keyframe file";
		
		BoneFileParser.loadKeyframes( keyframeFile, this );
	}
	
	/**
	 * Adds an animation to the animations map
	 * 
	 * @param animationName The animation's name
	 * @param animation The animation
	 */
	protected void addAnimation( Animation animation ) {
		
		assert( animation != null ) : "Cannot  set a null animation";
				
		this.animations.put( animation.getName(), animation );
	}
	
	/**
	 * Adds an animation keyframe to the Bone
	 * 
	 * @param keyframeAngle
	 * @param keyframeLength
	 * @param keyframeWidth
	 */
	protected void addKeyframe( Quaternion keyframeAngle, float keyframeLength, float keyframeWidth ) {
		
		assert keyframeAngle != null : "Cannot set a null angle";

		keyframeAngles.add( keyframeAngle );
		keyframeLengths.add( keyframeLength );
		keyframeBoneWidths.add( keyframeWidth );
	}

	/**
	 * Sets the node's current animation to the 
	 * 
	 * @param animationName
	 */
	public void setAnimation( String animationName ) {
		
		assert animationName != null : "Cannot set an animation with a null name";
		assert animations.containsKey( animationName ) : "Cannot set a non-existing animation";

		if( animations.containsKey( animationName ) ) {
			Animation newAnimation = animations.get( animationName ); 
			
			if( actualAnimation != null && actualAnimation.loops() ) {
				lastAnimation = actualAnimation;
			}
			
			this.actualAnimation = newAnimation;

			this.actualKeyframeNumber = actualAnimation.getInterval().getBegin() - 1;
		
			this.fetchNextKeyframe();
		
			this.elapsedTicks = 0;
		}
		
		for( Bone child : boneChildren ) {
			child.setAnimation( animationName );
		}	
	}
	
	/**
	 * Update the bone''s animation
	 * @param timePerFrame The time passed since the last update, in seconds 
	 */
	public void updateAnimation( float timePerFrame ) {
		
		assert timePerFrame >= 0.0 : "The time between frames is always greater or equal than zero";
		
		if( actualAnimation != null ) {
			elapsedTicks += timePerFrame;		
			
			if( elapsedTicks >= this.actualKeyframeLength ) {
				elapsedTicks -= this.actualKeyframeLength;
				this.fetchNextKeyframe();
				
				this.localRotation.set( actualKeyframeQuaternion.x, actualKeyframeQuaternion.y, actualKeyframeQuaternion.z, actualKeyframeQuaternion.w );
			} else {
				this.localRotation.set( actualKeyframeQuaternion.x, actualKeyframeQuaternion.y, actualKeyframeQuaternion.z, actualKeyframeQuaternion.w );
				this.localRotation.slerp( nextKeyframeQuaternion, (float)elapsedTicks / (float)this.actualKeyframeLength );
				this.localRotation.normalize();
				
				float deltaWidth = (nextKeyframeWidth - actualKeyframeWidth) / actualKeyframeLength * elapsedTicks;
				this.endJoint.setLocalTranslation( new Vector3f( 0, actualKeyframeWidth + deltaWidth, 0 ) );
				endJoint.updateGeometricState( 0.0f, true );
				
				this.localRotation.normalize();
			}
		}
		
		for( Bone child : boneChildren ) {
			child.updateAnimation( timePerFrame );
		}
	}
	
	/**
	 * Loads the next keyframe from the animation
	 */
	private void fetchNextKeyframe() {
		
		// If animation does not loop, fetch last animation's frame
		if( actualKeyframeNumber == actualAnimation.getInterval().getEnd()
			&& !actualAnimation.loops() 
			&& lastAnimation != null ) {
			this.actualKeyframeNumber = lastAnimation.getInterval().getBegin() - 1;
			this.actualAnimation = lastAnimation;
			this.fetchNextKeyframe();
		}
		
		this.actualKeyframeNumber = actualKeyframeNumber == actualAnimation.getInterval().getEnd() ?
														actualAnimation.getLoopFrame() :
														actualKeyframeNumber + 1;
		int nextKeyframeNumber = actualKeyframeNumber == actualAnimation.getInterval().getEnd() ?
													actualAnimation.getLoopFrame() :
													actualKeyframeNumber + 1;
		
		this.actualKeyframeQuaternion = keyframeAngles.get( actualKeyframeNumber );
		this.actualKeyframeLength = keyframeLengths.get( actualKeyframeNumber );
		this.actualKeyframeWidth = keyframeBoneWidths.get( actualKeyframeNumber );
		
		this.setLocalRotation( new Quaternion( actualKeyframeQuaternion ) );
		this.endJoint.setLocalTranslation( new Vector3f( 0, actualKeyframeWidth, 0 ) );
		
		this.updateGeometricState( 0.0f, false );
		endJoint.updateGeometricState( 0.0f, false );
		
		this.nextKeyframeQuaternion = keyframeAngles.get( nextKeyframeNumber );
		this.nextKeyframeWidth = keyframeBoneWidths.get( nextKeyframeNumber );
	}
	
	/**
	 * Overloads the super class method to handle the bone children
	 * @param child
	 */
	public void attachChild( Bone child ) {
		
		assert child != null : "Cannot attach a null child";
		
		this.endJoint.attachChild( child );
		this.boneChildren.add( child );		
	}
	
	/**
	 * @param bone
	 * @return <code>true</code> if the given bone is this bone's child
	 */
	public boolean isChild( Bone bone ){
		
		assert bone != null : "Cannot check if a null bone is a child";
				
		return this.boneChildren.contains( bone );
	}
	
	public int getActualKeyframeNumber(){
		
		return this.actualKeyframeNumber;
	}
	
	public float getActualKeyframeLength(){
		
		return this.actualKeyframeLength;
	}
	
	public float getActualKeyframeWidth(){
		
		return this.actualKeyframeWidth;
	}
}
